package com.gtc.zsk.controller;

import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gtc.zsk.annotation.AuthCheck;
import com.gtc.zsk.common.BaseResponse;
import com.gtc.zsk.common.DeleteRequest;
import com.gtc.zsk.common.ErrorCode;
import com.gtc.zsk.common.ResultUtils;
import com.gtc.zsk.constant.HotKeyConstant;
import com.gtc.zsk.constant.UserConstant;
import com.gtc.zsk.exception.BusinessException;
import com.gtc.zsk.exception.ThrowUtils;
import com.gtc.zsk.manager.CounterManager;
import com.gtc.zsk.model.dto.question.*;
import com.gtc.zsk.model.dto.questionBank.QuestionBankQueryRequest;
import com.gtc.zsk.model.entity.Question;
import com.gtc.zsk.model.entity.User;
import com.gtc.zsk.model.enums.QuestionVipEnum;
import com.gtc.zsk.model.enums.UserRoleEnum;
import com.gtc.zsk.model.vo.QuestionBankVO;
import com.gtc.zsk.model.vo.QuestionNoAnswerVO;
import com.gtc.zsk.model.vo.QuestionVO;
import com.gtc.zsk.service.QuestionService;
import com.gtc.zsk.service.UserService;
import com.gtc.zsk.service.VipBankUserService;
import com.jd.platform.hotkey.client.callback.JdHotKeyStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.gtc.zsk.constant.UserConstant.*;

/**
 * 題目接口
 *
 * @author <a href="https://github.com/ligtc">程序员鱼皮</a>
 * @from <a href="https://www.code-nav.cn">编程导航学习圈</a>
 */
@RestController
@RequestMapping("/question")
@Slf4j
public class QuestionController {

    @Resource
    private QuestionService questionService;

    @Resource
    private UserService userService;

    @Resource
    private VipBankUserService vipBankUserService;

    @Resource
    private CounterManager counterManager;

    // region 增删改查

    /**
     * 创建題目
     *
     * @param questionAddRequest
     * @param request
     * @return
     */
    @PostMapping("/add")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(questionAddRequest == null, ErrorCode.PARAMS_ERROR);
        // todo 在此处将实体类和 DTO 进行转换
        Question question = new Question();
        BeanUtils.copyProperties(questionAddRequest, question);
        //手动转换
        List<String> tags = questionAddRequest.getTags();
        if (tags != null)
            question.setTags(JSONUtil.toJsonStr(tags));
        // 数据校验
        questionService.validQuestion(question, true);
        // todo 填充默认值
        User loginUser = userService.getLoginUser(request);
        question.setUserId(loginUser.getId());
        // 写入数据库
        boolean result = questionService.save(question);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        // 返回新写入的数据 id
        long newQuestionId = question.getId();
        return ResultUtils.success(newQuestionId);
    }

    /**
     * 删除題目
     *
     * @param deleteRequest
     * @param request
     * @return
     */
    @PostMapping("/delete")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> deleteQuestion(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User user = userService.getLoginUser(request);
        long id = deleteRequest.getId();
        // 判断是否存在
        Question oldQuestion = questionService.getById(id);
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人或管理员可删除
        if (!oldQuestion.getUserId().equals(user.getId()) && !userService.isAdmin(request)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 操作数据库
//        boolean result = questionService.removeById(id);
//        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        questionService.singleDeleteQuestion(id); //删除题目的同时删除题目题库关联
        return ResultUtils.success(true);
    }

    /**
     * 更新題目（仅管理员可用）
     *
     * @param questionUpdateRequest
     * @return
     */
    @PostMapping("/update")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> updateQuestion(@RequestBody QuestionUpdateRequest questionUpdateRequest) {
        if (questionUpdateRequest == null || questionUpdateRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // todo 在此处将实体类和 DTO 进行转换
        Question question = new Question();
        BeanUtils.copyProperties(questionUpdateRequest, question);
        //手动转换
        List<String> tags = questionUpdateRequest.getTags();
        if (tags != null)
            question.setTags(JSONUtil.toJsonStr(tags));
        // 数据校验
        questionService.validQuestion(question, false);
        // 判断是否存在
        long id = questionUpdateRequest.getId();
        Question oldQuestion = questionService.getById(id);
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 操作数据库
        boolean result = questionService.updateById(question);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }

    /**
     * 根据 id 获取題目（封装类）
     *
     * @param id
     * @return
     */
    @GetMapping("/get/vo")
    public BaseResponse<QuestionVO> getQuestionVOById(long id, HttpServletRequest request) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);

        // 生成 key
        String key = HotKeyConstant.QUESTION_NO_ANSWER_HOT_KEY_PREFIX + id;
        // 如果worker计算出是热key(5s内发来10次请求)  如果返回false则isHotKey方法会将这个key进行上报
        if (JdHotKeyStore.isHotKey(key)){
            Object cachedQuestionVO = JdHotKeyStore.get(key);
            if (cachedQuestionVO != null)
                return ResultUtils.success((QuestionVO) cachedQuestionVO);
        }

        // 查询数据库
        Question question = questionService.getById(id);
        ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR);
        QuestionVO questionVO = questionService.getQuestionVO(question, request);
        questionVO.setAnswer(null);

        // 如果判断是热key 则设置本地缓存
        JdHotKeyStore.smartSet(key, questionVO);
        //TODO 是不是会员题目？ 有没有会员权限？
        // 获取封装类
        return ResultUtils.success(questionVO);
    }

    @GetMapping("/get/answer/vo")
    public BaseResponse<String> getQuestionAnswerVOById(long id, HttpServletRequest request) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
        Question question;
                // 生成 key
        String key = HotKeyConstant.QUESTION_ANSWER_HOT_KEY_PREFIX + id;
        // 如果worker计算出是热key(5s内发来10次请求)  如果返回false则isHotKey方法会将这个key进行上报
        if (JdHotKeyStore.isHotKey(key)){
            Object cachedQuestion = JdHotKeyStore.get(key);
            if (cachedQuestion != null){
                question = (Question)cachedQuestion;
            } else
                question = questionService.getById(id);
        }else {
            // 不是热key则查询数据库
            question = questionService.getById(id);
        }
        ThrowUtils.throwIf(question == null, ErrorCode.NOT_FOUND_ERROR);
        JdHotKeyStore.smartSet(key, question);
        // 判断是否已登录
//        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
//        User currentUser = (User) userObj;
//        if (currentUser == null || currentUser.getId() == null) {
//            question.setAnswer(NOT_LOGIN_QUESTION_ANSWER);
//            return ResultUtils.success(question.getAnswer());
//        }
//        // 从数据库查询（追求性能的话可以注释，直接走缓存）
//        long userId = currentUser.getId();
//        currentUser = userService.getById(userId);
//        if (currentUser == null) {
//            question.setAnswer(NOT_LOGIN_QUESTION_ANSWER);
//            return ResultUtils.success(question.getAnswer());
//        }
        Object loginUserId = StpUtil.getLoginIdDefaultNull();
        if (loginUserId == null){
            question.setAnswer(NOT_LOGIN_QUESTION_ANSWER);
            return ResultUtils.success(question.getAnswer());
        }
        User currentUser = userService.getById((String) loginUserId);
        if (currentUser == null){
            question.setAnswer(NOT_LOGIN_QUESTION_ANSWER);
            return ResultUtils.success(question.getAnswer());
        }
        //不是管理员的话  就进行检测和处理爬虫
        if (UserRoleEnum.getEnumByValue(currentUser.getUserRole()) == UserRoleEnum.USER)
            crawlerDetect(currentUser.getId());
        // 管理员 或者 本人 可以直接访问
        boolean isAdmin = userService.isAdmin(currentUser);
        if (isAdmin || question.getUserId().equals(currentUser.getId()))
            return ResultUtils.success(question.getAnswer());

        // 走到这里说明用户登录了  并且该用户既不是管理员 也不是创建问题的用户
        //TODO 是不是会员题目？ 有没有会员权限？
        QuestionVipEnum questionVipEnum = QuestionVipEnum.getEnumByValue(question.getIsVip());
        //  如果是会员题目
        if(QuestionVipEnum.VIP == questionVipEnum){
            boolean result = vipBankUserService.checkUserVip(id, currentUser.getId());
            if (!result)
                question.setAnswer(NOT_VIP_QUESTION_ANSWER);

        }
        return ResultUtils.success(question.getAnswer());
    }


    /**
     * 检测爬虫
     *
     * @param loginUserId
     */
    private void crawlerDetect(long loginUserId) {
        // 调用多少次时告警
        final int WARN_COUNT = 20;
        final int SECOND_WARN_COUNT = 25;
        // 超过多少次封号
        final int BAN_COUNT = 30;
        // 拼接访问 key
        String key = String.format("user:access:%s", loginUserId);
        // 一分钟内访问次数，180秒过期
        long count = counterManager.incrAndGetCounter(key, 1, TimeUnit.MINUTES, 180);
        // 是否封号
        if (count > BAN_COUNT) {
            // 踢下线
            StpUtil.kickout(loginUserId);
            // 封号
            User updateUser = new User();
            updateUser.setId(loginUserId);
            updateUser.setStatus(1);
            userService.updateById(updateUser);
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "您的访问过于频繁，当前账号已被封禁，请联系管理员解封");
        }
        // 是否告警
        if (count == WARN_COUNT) {
            // 可以改为向管理员发送邮件通知
            throw new BusinessException(110, "警告：您近期对题目的访问频率过高！");
        }
        if (count == SECOND_WARN_COUNT) {
            // 可以改为向管理员发送邮件通知
            throw new BusinessException(110, "警告：疑似爬虫，请规范自身行为！");
        }
    }


    /**
     * 分页获取題目列表（仅管理员可用）
     *
     * @param questionQueryRequest
     * @return
     */
    @PostMapping("/list/page")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Page<Question>> listQuestionByPage(@RequestBody QuestionQueryRequest questionQueryRequest) {
        ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR);
        Page<Question> questionPage = questionService.listQuestionByPage(questionQueryRequest);
        return ResultUtils.success(questionPage);
    }

    /**
     * 分页获取題目列表（封装类）  没有答案
     *
     * @param questionQueryRequest
     * @param request
     * @return
     */
    @PostMapping("/list/page/vo")
    public BaseResponse<Page<QuestionNoAnswerVO>> listQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                                       HttpServletRequest request) {
        long current = questionQueryRequest.getCurrent();
        long size = questionQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        Page<Question> questionPage = questionService.page(new Page<>(current, size),
                questionService.getQueryWrapper(questionQueryRequest));
        // 获取封装类
        return ResultUtils.success(questionService.getQuestionNoAnswerVOPage(questionPage, request));
    }


    /**
     * 分页获取題目列表（封装类）  限流版  使用的是热点限流参数（根据参数统计  而不只是根据接口调用次数统计）
     *        2.编码定义资源 + 编程式定义规则
     * @param questionQueryRequest
     * @param request
     * @return
     */
    @PostMapping("/list/page/vo/sentinel")
    public BaseResponse<Page<QuestionNoAnswerVO>> listQuestionVOByPageSentinel(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                                       HttpServletRequest request) {
        long current = questionQueryRequest.getCurrent();
        long size = questionQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        //基于 IP 限流
        String remoteAddr = request.getRemoteAddr();
        Entry entry = null;
        try {
            //每调用一次 上报几次资源 传递的参数是remoteAddr
            entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr);
            // 被保护的业务逻辑
                // 查询数据库
            Page<Question> questionPage = questionService.page(new Page<>(current, size),
                    questionService.getQueryWrapper(questionQueryRequest));
                // 获取封装类
            return ResultUtils.success(questionService.getQuestionNoAnswerVOPage(questionPage, request));
        } catch (Throwable ex){
            //判断异常类型
            //如果不是sentinel异常（即自己业务中发生的异常） 则需要自己上报异常
            if (!BlockException.isBlockException(ex)){
                Tracer.trace(ex);
                return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "业务出现错误！");
            }
            //如果是熔断后的降级异常
            if (ex instanceof DegradeException)
                return handleFallback(questionQueryRequest, request, ex);
            //如果是限流异常
            return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "您的访问过于频繁，请稍后再试！");
        } finally {
            if (entry != null)
                entry.exit(1, remoteAddr);
        }

    }

    public BaseResponse<Page<QuestionNoAnswerVO>> handleFallback(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                             HttpServletRequest request, Throwable ex) {
        // 可以返回本地数据或空数据
        return ResultUtils.success(null);
    }


    /**
     * 分页获取当前登录用户创建的題目列表
     *
     * @param questionQueryRequest
     * @param request
     * @return
     */
    @PostMapping("/my/list/page/vo")
    public BaseResponse<Page<QuestionVO>> listMyQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                                 HttpServletRequest request) {
        ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR);
        // 补充查询条件，只查询当前登录用户的数据
        User loginUser = userService.getLoginUser(request);
        questionQueryRequest.setUserId(loginUser.getId());
        long current = questionQueryRequest.getCurrent();
        long size = questionQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        Page<Question> questionPage = questionService.page(new Page<>(current, size),
                questionService.getQueryWrapper(questionQueryRequest));
        // 获取封装类
        return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));
    }

    /**
     * 编辑題目（给用户使用）
     *
     * @param questionEditRequest
     * @param request
     * @return
     */
    @PostMapping("/edit")
    public BaseResponse<Boolean> editQuestion(@RequestBody QuestionEditRequest questionEditRequest, HttpServletRequest request) {
        if (questionEditRequest == null || questionEditRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // todo 在此处将实体类和 DTO 进行转换
        Question question = new Question();
        BeanUtils.copyProperties(questionEditRequest, question);
        //手动转换
        List<String> tags = questionEditRequest.getTags();
        if (tags != null)
            question.setTags(JSONUtil.toJsonStr(tags));
        // 数据校验
        questionService.validQuestion(question, false);
        User loginUser = userService.getLoginUser(request);
        // 判断是否存在
        long id = questionEditRequest.getId();
        Question oldQuestion = questionService.getById(id);
        ThrowUtils.throwIf(oldQuestion == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人或管理员可编辑
        if (!oldQuestion.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        // 操作数据库
        boolean result = questionService.updateById(question);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }


    @PostMapping("/search/page/vo")
    public BaseResponse<Page<QuestionNoAnswerVO>> searchQuestionVOByPage(@RequestBody QuestionQueryRequest questionQueryRequest,
                                                                 HttpServletRequest request) {
        long size = questionQueryRequest.getPageSize();
        // 限制爬虫
        ThrowUtils.throwIf(size > 200, ErrorCode.PARAMS_ERROR);
        Page<Question> questionPage = questionService.searchFromEs(questionQueryRequest);
        return ResultUtils.success(questionService.getQuestionNoAnswerVOPage(questionPage, request));
    }

    @PostMapping("/delete/batch")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> batchDeleteQuestions(@RequestBody QuestionBatchDeleteRequest questionBatchDeleteRequest,
                                                      HttpServletRequest request) {
        ThrowUtils.throwIf(questionBatchDeleteRequest == null, ErrorCode.PARAMS_ERROR);
        questionService.batchDeleteQuestions(questionBatchDeleteRequest.getQuestionIdList());
        return ResultUtils.success(true);
    }

    @PostMapping("/vip/batch")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> batchAlterVipQuestions(@RequestBody QuestionBatchAlterVipRequest questionBatchAlterVipRequest) {
        ThrowUtils.throwIf(questionBatchAlterVipRequest == null, ErrorCode.PARAMS_ERROR);
        Integer isVip = questionBatchAlterVipRequest.getIsVip();
        List<Long> questionIdList = questionBatchAlterVipRequest.getQuestionIdList();
        questionService.batchAlterVipQuestions(questionIdList, isVip);
        return ResultUtils.success(true);
    }

    /**
     * AI 生成题目（仅管理员可用）
     *
     * @param questionAIGenerateRequest 请求参数
     * @param request HTTP 请求
     * @return 是否生成成功
     */
    @PostMapping("/ai/generate/question")
    @SaCheckRole(UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> aiGenerateQuestions(@RequestBody QuestionAIGenerateRequest questionAIGenerateRequest, HttpServletRequest request) {
        String questionType = questionAIGenerateRequest.getQuestionType();
        int number = questionAIGenerateRequest.getNumber();
        // 校验参数
        ThrowUtils.throwIf(StrUtil.isBlank(questionType), ErrorCode.PARAMS_ERROR, "题目类型不能为空");
        ThrowUtils.throwIf(number <= 0, ErrorCode.PARAMS_ERROR, "题目数量必须大于 0");
        ThrowUtils.throwIf(number > 20, ErrorCode.PARAMS_ERROR, "题目数量不能大于 0");
        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);
        // 调用 AI 生成题目服务
        questionService.aiGenerateQuestions(questionType, number, loginUser);
        // 返回结果
        return ResultUtils.success(true);
    }

    // endregion
}
